const Router = require('koa-router');
const util = require('ethereumjs-util')
const Web3 = require('web3')
const Tx = require('ethereumjs-tx');
const FACTORY_ABI = require('../abis/factory.json')
const PAIR_ABI = require('../abis/pair.json')
const ROUTER_ABI = require('../abis/router.json')
const { MaxUint256 } = require('@ethersproject/constants')
const { parseAmount, formatAmount, accDiv, calculateGasMargin, accMul, toFixed } = require('./Format')
// bsc pancakeswap
const chainId = "0x38" //bsc主网
var web3 = new Web3();
web3.setProvider(new Web3.providers.HttpProvider("https://bsc-dataseed1.binance.org"));
const pancakeRouter = "0x10ED43C718714eb63d5aA57B78B54704E256024E" //pancakeRouter
const factory = "0xcA143Ce32Fe78f1f7019d7d551a6402fC5350c73" //工厂合约

// rinkeby sushiswap
// const chainId = "0x4" //rinkeby
// var web3 = new Web3();
// web3.setProvider(new Web3.providers.HttpProvider("https://rinkeby.infura.io/v3/9aa3d95b3bc440fa88ea12eaa4456161"));
// const pancakeRouter = "0x1b02dA8Cb0d097eB8D57A175b88c7D8b47997506" // sushiRouter
// const factory = "0xc35dadb65012ec5796536bd9864ed8773abc74c4" //工厂合约

const USDT = {
    contract: "0x55d398326f99059fF775485246999027B3197955",
    decimal: 18,
    symbol: 'USDT'
}
const WBNB = {
    contract: "0xbb4CdB9CBd36B01bD1cBaEBF2De08d9173bc095c",
    decimal: 18,
    symbol: 'BNB'

}
const BUSD = {
    contract: "0xe9e7cea3dedca5984780bafc599bd69add087d56",
    decimal: 18,
    symbol: 'BUSD'
}
const WETH = {
    contract: "0xc778417e063141139fce010982780140aa0cd5ab",
    decimal: 18,
    symbol: 'ETH'
}

let router = new Router();

function handleError(e){
    if(e.includes("TRANSFER_FROM_FAILED")){
        return '余额不足或者未授权'
    }else if(e.includes('transaction underpriced')){
        return '当前使用nonce不是最新或者gas费太低'
    }else{
        return e
    }
    
    
}

// 获取交易对合约
async function getPair(tokenA, tokenB){
    const contract = new web3.eth.Contract(
        FACTORY_ABI,
        factory
    );
    const lpContract = await contract.methods.getPair(tokenA, tokenB).call()
    return lpContract
}

// 获取价格
async function getPrice(pair,unit){
    const contract = new web3.eth.Contract(
        PAIR_ABI,
        pair
    );
    const reservePromise = contract.methods.getReserves().call()
    const token0Promise = contract.methods.token0().call()
    const token1Promise = contract.methods.token1().call()
    const [reserve, token0, token1] = await Promise.all([reservePromise, token0Promise, token1Promise])
    if(token0.toLowerCase() == unit.toLowerCase()){
        const erc20 = new web3.eth.Contract(
            PAIR_ABI,
            token1
        );
        const decimals1 = await erc20.methods.decimals().call()
        return accDiv(formatAmount(reserve._reserve0, 18),formatAmount(reserve._reserve1, decimals1))  
    }else{
        const erc20 = new web3.eth.Contract(
            PAIR_ABI,
            token0
        );
        const decimals0 = await erc20.methods.decimals().call()
        return accDiv(formatAmount(reserve._reserve1, 18),formatAmount(reserve._reserve0, decimals0))
    } 
}

async function getReserves(pair){
    const contract = new web3.eth.Contract(
        PAIR_ABI,
        pair
    );
    const reservePromise = contract.methods.getReserves().call()
    const token0Promise = contract.methods.token0().call()
    const token1Promise = contract.methods.token1().call()
    const [reserve, token0, token1] = await Promise.all([reservePromise, token0Promise, token1Promise])
    return { reserve0: reserve._reserve0, reserve1: reserve._reserve1, token0, token1 }
}

async function getAmountOut(reserve0, reserve1, token0, token1, amountIn, tokenIn){
    const contract = new web3.eth.Contract(
        ROUTER_ABI,
        pancakeRouter
    );
    let amountOut;
    if(token0.toLowerCase() == tokenIn.toLowerCase()){
        amountOut = await contract.methods.getAmountOut(amountIn, reserve0, reserve1).call()
    }else if(token1.toLowerCase() == tokenIn.toLowerCase()){
        amountOut = await contract.methods.getAmountOut(amountIn, reserve1, reserve0).call()
    }else{
        amountOut = 0
    }
    return amountOut;
}

async function swapExactTokensForTokens(amountIn, amountOutMin, path, fromAddr, privateKey, myGasPrice, nonce){
    return new Promise(async (resolve, reject)=>{
        const myContract = new web3.eth.Contract(
            ROUTER_ABI,
            pancakeRouter
        );
        try{
            const gas = await myContract.methods.swapExactTokensForTokens(amountIn.toString(), amountOutMin.toString(), path, fromAddr, 1950523336).estimateGas({from: fromAddr, value: 0})
            const code = myContract.methods.swapExactTokensForTokens(amountIn.toString(), amountOutMin.toString(), path, fromAddr, 1950523336).encodeABI();
            // const nonce = await web3.eth.getTransactionCount(fromAddr);
            const gasPrice = await web3.eth.getGasPrice();
            const rawTx = {
                "from": fromAddr,
                "to": pancakeRouter,
                "nonce": nonce,
                'gasPrice': myGasPrice? Number(accMul(myGasPrice),1000000000): Number(gasPrice),
                'gasLimit': calculateGasMargin(gas),
                "chainId": chainId,
                "value": 0,
                "data": code,
            };
            let tx = new Tx(rawTx);
            tx.sign(privateKey);
            let serializedTx = tx.serialize();
            web3.eth.sendSignedTransaction("0x" + serializedTx.toString("hex"))
            .on("transactionHash", (hash) => {
                console.log("闪兑hash",hash);
                resolve(hash)
            })
            .on("error", (err) => {
                console.log("闪兑出错",err);
                reject(err+"")
            }); 
            }
        catch(e){
            reject(e+"")
        }
    })
}
async function swapExactTokensForETH(amountIn, amountOutMin, path, fromAddr, privateKey, myGasPrice, nonce){
    return new Promise(async (resolve, reject)=>{
        const myContract = new web3.eth.Contract(
            ROUTER_ABI,
            pancakeRouter
        );
        try {
            const gas = await myContract.methods.swapExactTokensForETH(amountIn.toString(), amountOutMin.toString(), path, fromAddr, 1950523336).estimateGas({from: fromAddr, value: 0})
            const code = myContract.methods.swapExactTokensForETH(amountIn.toString(), amountOutMin.toString(), path, fromAddr, 1950523336).encodeABI();
            // const nonce = await web3.eth.getTransactionCount(fromAddr);
            const gasPrice = await web3.eth.getGasPrice();
            const rawTx = {
                "from": fromAddr,
                "to": pancakeRouter,
                "nonce": nonce,
                'gasPrice': myGasPrice? Number(accMul(myGasPrice),1000000000): Number(gasPrice),
                'gasLimit': calculateGasMargin(gas),
                "chainId": chainId,
                "value": 0,
                "data": code,
            };
            let tx = new Tx(rawTx);
            tx.sign(privateKey);
            let serializedTx = tx.serialize();
            web3.eth.sendSignedTransaction("0x" + serializedTx.toString("hex"))
            .on("transactionHash", (hash) => {
                console.log("闪兑hash",hash);
                resolve(hash)
            })
            .on("error", (err) => {
                console.log("闪兑出错",err);
                reject(err+"")
            }); 
        }catch(e){
            reject(e+"")
        }
        
    })
}
async function swapExactETHForTokens(amountIn, amountOutMin, path, fromAddr, privateKey, myGasPrice, nonce){
    console.log(nonce,"noncenoncenonce");
    return new Promise(async (resolve, reject)=>{
        const myContract = new web3.eth.Contract(
            ROUTER_ABI,
            pancakeRouter
        );
        try{
            const gas = await myContract.methods.swapExactETHForTokens(amountOutMin.toString(), path, fromAddr, 1950523336).estimateGas({from: fromAddr, value: amountIn})
            const code = myContract.methods.swapExactETHForTokens(amountOutMin.toString(), path, fromAddr, 1950523336).encodeABI();
            const gasPrice = await web3.eth.getGasPrice();
            const rawTx = {
                "from": fromAddr,
                "to": pancakeRouter,
                "nonce": nonce,
                'gasPrice': myGasPrice? Number(accMul(myGasPrice),1000000000): Number(gasPrice),
                'gasLimit': calculateGasMargin(gas),
                "chainId": chainId,
                "value": Number(amountIn),
                "data": code,
            };
            let tx = new Tx(rawTx);
            tx.sign(privateKey);
            let serializedTx = tx.serialize();
            web3.eth.sendSignedTransaction("0x" + serializedTx.toString("hex"))
            .on("transactionHash", (hash) => {
                console.log("闪兑hash",hash);
                resolve(hash)
            })
            .on("error", (err) => {
                console.log("闪兑出错",err);
                reject(err+"")
            })
        }catch(e){
            reject(e+"")
        }
    })
}

router.post('/getPrice', async (ctx, next) => {
    const { contract, uint } = ctx.request.body;
    let myUint = BUSD
    if(uint.toLowerCase() == 'bnb'){
        myUint = WBNB
    }
    else if(uint.toLowerCase() == 'usdt'){
        myUint = USDT
    }else{
        myUint = BUSD
    }
    const lpContract = await getPair(contract,myUint.contract)
    if(lpContract == '0x0000000000000000000000000000000000000000'){
        ctx.body = {
            code: 0,
            price: "没有此交易对",
            status: false,
        };
        return;
    }else{
        let price = await getPrice(lpContract,myUint.contract);
        price = price + ' ' + myUint.symbol
        ctx.body = {
        code: 0,
        price: price,
        status: true,
        };
    }
});
  

router.post('/swap', async (ctx, next) => {
    let {amountIn, tokenIn , tokenOut, slippage, privateKey,  myGasPrice, num} = ctx.request.body;
    let lpContract = ""
    if(tokenIn.toLowerCase() == 'usdt'){
        tokenIn = USDT.contract
    }
    if(tokenIn.toLowerCase() == 'busd'){
        tokenIn = BUSD.contract
    }
    if(tokenOut.toLowerCase() == 'usdt'){
        tokenOut = USDT.contract
    }
    if(tokenOut.toLowerCase() == 'busd'){
        tokenOut = BUSD.contract
    }
    if(tokenIn.toLowerCase() == 'bnb'){
        tokenOut = web3.utils.toChecksumAddress(tokenOut);
        lpContract = await getPair(WBNB.contract, tokenOut)
    }
    else if(tokenOut.toLowerCase() == 'bnb'){
        tokenIn = web3.utils.toChecksumAddress(tokenIn);
        lpContract = await getPair(tokenIn, WBNB.contract)
    }
    else {
        tokenIn = web3.utils.toChecksumAddress(tokenIn);
        tokenOut = web3.utils.toChecksumAddress(tokenOut);
        lpContract = await getPair(tokenIn, tokenOut)
    }
    if(lpContract == '0x0000000000000000000000000000000000000000'){
        ctx.body = {
            code: 0,
            price: "没有此交易对",
            status: false,
        };
        return;
    }
 
    const { reserve0, reserve1, token0, token1 } = await getReserves(lpContract)
    let erc20
    if(tokenIn.toLowerCase() == 'bnb'){
        erc20 = new web3.eth.Contract(PAIR_ABI, WBNB.contract);
    }else{
        erc20 = new web3.eth.Contract(PAIR_ABI, tokenIn);
    }
    const decimalsIn = await erc20.methods.decimals().call()
    amountIn = parseAmount(amountIn, decimalsIn) //格式化输入数量

    let amountOut = 0;
    if(tokenIn.toLowerCase() == 'bnb'){
        amountOut = await getAmountOut(reserve0, reserve1, token0, token1, amountIn, WBNB.contract)
    }else{
        amountOut = await getAmountOut(reserve0, reserve1, token0, token1, amountIn, tokenIn)
    }
    const amountOutMin = toFixed(accDiv(accMul(amountOut, 100 - slippage), 100))
    const _privateKey = Buffer.from(privateKey,"hex");
    let address = util.bufferToHex(util.privateToAddress(_privateKey))
    address = web3.utils.toChecksumAddress(address);
    let hashList = []
    try{
        let hash = ""
        // bsc
        let nonce = await web3.eth.getTransactionCount(address);
        num = num ? num :1
        for(let i=0 ; i<num ; i++){
            if(tokenIn.toLowerCase() != 'bnb' && tokenOut.toLowerCase() != 'bnb'){
                hash = await swapExactTokensForTokens(amountIn, amountOutMin, [tokenIn, tokenOut], address, _privateKey, myGasPrice, nonce)
                nonce++
            }else if(tokenIn.toLowerCase() == 'bnb'){
                hash = await swapExactETHForTokens(amountIn, amountOutMin, [WBNB.contract, tokenOut], address, _privateKey, myGasPrice, nonce)
                nonce++
            }else if(tokenOut.toLowerCase() == 'bnb'){
                hash = await swapExactTokensForETH(amountIn, amountOutMin, [tokenIn, WBNB.contract], address, _privateKey, myGasPrice, nonce)
                nonce++
            }
            hashList.push(hash)
        }
        ctx.body = {
            code: 0,
            data: hashList,
            status: true,
        };
    }catch(e){
        ctx.body = {
            code: 0,
            data:hashList,
            error: handleError(e),
            status: false,
        };
    }
});

router.post('/ethSwap', async (ctx, next) => {
    let {amountIn, tokenIn , tokenOut, slippage, myGasPrice, privateKey, num} = ctx.request.body;
    console.log(amountIn, tokenIn , tokenOut, slippage, myGasPrice, privateKey,"amountIn, tokenIn , tokenOut, slippage, myGasPrice, privateKey");
    let lpContract = ""
    if(tokenIn.toLowerCase() == 'eth'){
        tokenOut = web3.utils.toChecksumAddress(tokenOut);
        lpContract = await getPair(WETH.contract, tokenOut)
    }
    else if(tokenOut.toLowerCase() == 'eth'){
        tokenIn = web3.utils.toChecksumAddress(tokenIn);
        lpContract = await getPair(tokenIn, WETH.contract)
    }
    else {
        tokenIn = web3.utils.toChecksumAddress(tokenIn);
        tokenOut = web3.utils.toChecksumAddress(tokenOut);
        lpContract = await getPair(tokenIn, tokenOut)
    }
    if(lpContract == '0x0000000000000000000000000000000000000000'){
        ctx.body = {
            code: 0,
            price: "没有此交易对",
            status: false,
        };
        return;
    }
 
    const { reserve0, reserve1, token0, token1 } = await getReserves(lpContract)
    let erc20
    if(tokenIn.toLowerCase() == 'eth'){
        erc20 = new web3.eth.Contract(PAIR_ABI, WETH.contract);
    }else{
        erc20 = new web3.eth.Contract(PAIR_ABI, tokenIn);
    }
    const decimalsIn = await erc20.methods.decimals().call()
    amountIn = parseAmount(amountIn, decimalsIn) //格式化输入数量

    let amountOut = 0;

    if(tokenIn.toLowerCase() == 'eth'){
        amountOut = await getAmountOut(reserve0, reserve1, token0, token1, amountIn, WETH.contract)
    }else{
        amountOut = await getAmountOut(reserve0, reserve1, token0, token1, amountIn, tokenIn)
    }
    const amountOutMin = toFixed(accDiv(accMul(amountOut, 100 - slippage), 100))
    const _privateKey = Buffer.from(privateKey,"hex");
    let address = util.bufferToHex(util.privateToAddress(_privateKey))
    address = web3.utils.toChecksumAddress(address);
    let hashList = []
    try{
        let hash = ""
        // bsc
        let nonce = await web3.eth.getTransactionCount(address);
        num = num ? num :1
        for(let i=0 ; i<num ; i++){
            if(tokenIn.toLowerCase() != 'eth' && tokenOut.toLowerCase() != 'eth'){
                hash = await swapExactTokensForTokens(amountIn, amountOutMin, [tokenIn, tokenOut], address, _privateKey, myGasPrice, nonce)
                nonce ++
            }else if(tokenIn.toLowerCase() == 'eth'){
                hash = await swapExactETHForTokens(amountIn, amountOutMin, [WETH.contract, tokenOut], address, _privateKey, myGasPrice, nonce)
                nonce ++
            }else if(tokenOut.toLowerCase() == 'eth'){
                hash = await swapExactTokensForETH(amountIn, amountOutMin, [tokenIn, WETH.contract], address, _privateKey, myGasPrice, nonce)
                nonce ++
            }
            hashList.push(hash)
        }
        ctx.body = {
            code: 0,
            data: hashList,
            status: true,
        };
    }catch(e){
        ctx.body = {
            code: 0,
            data:hashList,
            error: handleError(e),
            status: false,
        };
    }
});
module.exports = router;
